/*
  S.M.A.C.K - An operating system kernel
  Copyright (C) 2010,2011 Mattias Holm and Kristian Rietveld
  For licensing and a full list of authors of the kernel, see the files
  COPYING and AUTHORS.

	modified by: Benjamin vd Burgh(0746894) & Ricardo Meijer (0801496)
*/

#include <vfs/wfs.h>
#include <vfs/vfs.h>

#include <stdio.h>
#include <string.h>
#include <vm.h>
#include <bittools.h>

/* FIXME: Written and tested on little endian.  Might need fixes
 * for big endian.
 */

#define MAX_FS 8

typedef struct
{
  dev_t *dev;
  dev_minor_t minor;

  uint16_t *block_table;
} wfs_info_t;

typedef struct
{
  wfs_info_t *info;
  uint16_t start_of_file;
  size_t size;
} wfs_node_info_t;

static wfs_info_t wfs_info_array[MAX_FS];
static uint8_t current_wfs = 0;

static size_t wfs_file_read(vfs_file_t *file, void *buff, size_t len);
static size_t wfs_file_write(vfs_file_t *file, const void *buff, size_t len);
static int wfs_file_open(vfs_vnode_t *vnode);
static int wfs_file_close(vfs_file_t *file);
static uint16_t wfs_get_free_block(uint16_t *block_table);
static uint16_t wfs_get_current_block(wfs_node_info_t *ninfo, off_t off, uint16_t *block_position);

static vfs_fileops_t wfs_file_fileops =
{
  .read = wfs_file_read,
  .write = wfs_file_write,
  .open = wfs_file_open,
  .close = wfs_file_close
};

static int wfs_directory_readdir(vfs_vnode_t *node, size_t *files_in_dir, vfs_vnode_t ***files);
static int wfs_mkdir(vfs_vnode_t *parent, const char *name);
static int wfs_rmdir(vfs_vnode_t *node);
static uint32_t wfs_get_block_offset(wfs_info_t *fs, int block);
static void wfs_write_block_table (wfs_info_t *info);

static vfs_fileops_t wfs_directory_fileops =
{
    .readdir = wfs_directory_readdir,
    .mkdir = wfs_mkdir,
    .rmdir = wfs_rmdir
};

static uint32_t
wfs_file_entry_get_size(const wfs_file_entry_t *entry)
{
  return entry->size & WFS_SIZE_MASK;
}

static bool
wfs_file_entry_is_empty(const wfs_file_entry_t *entry)
{
  if (entry->filename[0] == 0 || entry->start_block == WFS_BLOCK_FREE)
    return 1;

  return 0;
}

static bool
wfs_file_entry_is_directory(const wfs_file_entry_t *entry)
{
  if ((entry->size & WFS_SIZE_IS_DIRECTORY) == WFS_SIZE_IS_DIRECTORY)
    return true;

  return false;
}

//! Create directory with name under parent
static int
wfs_mkdir(vfs_vnode_t *parent, const char *name) {
    wfs_node_info_t *ninfo = (wfs_node_info_t *)&parent->fs_data;
    wfs_info_t *info = ninfo->info;
    uint32_t offset;
    int block;
    int i;
    
    if (parent->type != VFS_DIR) {
        printf("WFS: error: cannot create a subdirectory under the current working directory.\n");
        return -1;
    }

    /* Initialize file entry */
    wfs_file_entry_t new_dir;
    if (strlcpy(new_dir.filename, name, strnlen(name, 58)) > 58) {
        printf("WFS: error: filename to long.\n");
        return -1;
    }
    new_dir.size = WFS_SIZE_IS_DIRECTORY;
    new_dir.start_block = wfs_get_free_block(info->block_table);

    /* Search for a place in the parent directory entry to put the new subdirectory entry in */
    if (parent->parent->type == VFS_MOUNT) {  // Parent is root directory node
        wfs_file_entry_t entry;

        block = wfs_get_current_block(ninfo, 0, NULL);
        offset = wfs_get_block_offset(info, block - 1);
        dev_seek(info->dev, info->minor, offset, SEEK_SET);
        for (i = 0; i < 8; i++) {
            dev_read(info->dev, info->minor, &entry, sizeof (wfs_file_entry_t));
            if (wfs_file_entry_is_empty(&entry))  // An empty entry has been found
                break;
        }

        if (i == 8) {
            printf("WFS: error: no space available.\n");
            return -1;
        }
    }
    else {  // Parent is a subdirectory node
        offset = 0;
        wfs_file_entry_t entry;

        dev_seek(info->dev, info->minor, 0, SEEK_SET);
        for (i = 0; i < 64; i++) {
            dev_read(info->dev, info->minor, &entry, sizeof (wfs_file_entry_t));
            if (wfs_file_entry_is_empty(&entry)) {  // An empty entry has been found
                break;
            }
        }

        if (i == 8) {
            printf("WFS: error: no space available.\n");
            return -1;
        }
    }

    dev_seek(info->dev, info->minor, offset + (i * sizeof(wfs_file_entry_t)), SEEK_SET);
    dev_write(info->dev, info->minor, &new_dir, sizeof(wfs_file_entry_t));

    info->block_table[new_dir.start_block - 1] = WFS_BLOCK_EOF;
    wfs_write_block_table(info);

    /* Initialize the VFS vnode and add it*/
    vfs_vnode_t *new_node = vfs_mknode();
    new_node->parent = parent;

    if (strlcpy(new_node->name, name, strnlen(name, 58) + 1) > 58) {
        printf("WFS: error: filename to long.\n");
        return -1;
    }
    
    new_node->fs = parent->fs;
    new_node->type = VFS_DIR;
    new_node->size = WFS_SIZE_IS_DIRECTORY;
    new_node->fops = parent->fops;

    wfs_node_info_t *new_ninfo = (wfs_node_info_t *)&new_node->fs_data;

    new_ninfo->info = info;
    new_ninfo->start_of_file = new_dir.start_block;
    new_ninfo->size = WFS_SIZE_IS_DIRECTORY;

    vfs_dirp_addnode(parent, new_node);

    dev_seek(info->dev, info->minor, wfs_get_block_offset(new_ninfo->info, new_dir.start_block - 1), SEEK_SET);

    wfs_file_entry_t empty;
    memset(&empty, 0, sizeof(wfs_file_entry_t));
    for (int j = 0; j < 8; j++) {
        dev_write(info->dev, info->minor, &empty, sizeof(wfs_file_entry_t));
    }

    return 0;
}

static int
wfs_rmdir(vfs_vnode_t *node) {
    int i;
    wfs_node_info_t *ninfo = (wfs_node_info_t *)&node->fs_data;
    wfs_info_t *info = ninfo->info;
    int block = ninfo->start_of_file;
    wfs_file_entry_t rm_dir;
    wfs_node_info_t *pninfo = (wfs_node_info_t *)&node->parent->fs_data;
    
    if (node->type != VFS_DIR) {
        printf("WFS: error: failed to remove %s: not a directory.\n", node->name);
        return -1;
    }

    info->block_table[block - 1] = WFS_BLOCK_FREE;
    wfs_write_block_table(info);

    if (node->parent->type != VFS_MOUNT) {
        dev_seek(info->dev, info->minor, wfs_get_block_offset(info, pninfo->start_of_file - 1), SEEK_SET);
        for (i = 0; i < 8; i++) {
            dev_read(info->dev, info->minor, &rm_dir, sizeof(wfs_file_entry_t));
            if (rm_dir.start_block == block) {
                dev_seek(info->dev, info->minor, wfs_get_block_offset(info, pninfo->start_of_file - 1) + (i * sizeof(wfs_file_entry_t)), SEEK_SET);
                memset(&rm_dir,0,sizeof(wfs_file_entry_t));
                dev_write(info->dev, info->minor, &rm_dir, sizeof (wfs_file_entry_t));
                break;
            }
        }
    } else {
        dev_seek(info->dev, info->minor, 0, SEEK_SET);
        for (i = 0; i < 64; i++) {
            dev_read(info->dev, info->minor, &rm_dir, sizeof (wfs_file_entry_t));
            if (rm_dir.start_block == block) {
                dev_seek(info->dev, info->minor, (i * sizeof (wfs_file_entry_t)), SEEK_SET);
                memset(&rm_dir,0,sizeof(wfs_file_entry_t));
                dev_write(info->dev, info->minor, &rm_dir, sizeof (wfs_file_entry_t));
                break;
            }
        }
    }

    vfs_dirp_removenode(node->parent, node);

    return 0;
}

static int
wfs_directory_readdir(vfs_vnode_t *node,
        size_t *files_in_dir,
        vfs_vnode_t ***files) {
    int i;
    int processed = 0;
    int max_elements;
    uint32_t offset;
    int block;

    wfs_node_info_t *ninfo = (wfs_node_info_t *) & node->fs_data;
    wfs_info_t *info = ninfo->info;

    if (node->type != VFS_DIR) {
        printf("WFS: error: failed to remove %s: not a directory.\n",node->name);
        return -1;
    }
        
    if (node->special.dir.files_in_dir != 0
            && node->special.dir.files != NULL) {
        *files_in_dir = node->special.dir.files_in_dir;
        *files = node->special.dir.files;

        return 0;
    }

    if (!node->parent) {
        printf("WFS: error: corrupted VFS tree structure.\n");
        return -1;
    }

    if (node->parent->type == VFS_MOUNT)
        max_elements = WFS_N_FILES;
    else max_elements = 8;

    node->special.dir.files_in_dir = 0;

    block = wfs_get_current_block(ninfo, 0, NULL);

    if (node->parent->type != VFS_MOUNT)
        offset = wfs_get_block_offset(info, block - 1);
    else offset = 0;

    /* Count files in directory */
    dev_seek(info->dev, info->minor, offset, SEEK_SET);

    for (i = 0; i < max_elements; i++) {
        wfs_file_entry_t entry;
        dev_read(info->dev, info->minor, &entry, sizeof (wfs_file_entry_t));

        if (wfs_file_entry_is_empty(&entry))
            continue;

        node->special.dir.files_in_dir++;
    }

    node->special.dir.files = vfs_getdirp(node->special.dir.files_in_dir);

    /* Actually read the directory */
    dev_seek(info->dev, info->minor, offset, SEEK_SET);
    for (i = 0; i < max_elements; i++) {
        wfs_file_entry_t entry;
        vfs_vnode_t *new_node;
        wfs_node_info_t *new_node_info;

        dev_read(info->dev, info->minor, &entry, sizeof (wfs_file_entry_t));

        if (wfs_file_entry_is_empty(&entry))
            continue;

        //! Allocates a new empty unconnected vnode
        new_node = node->special.dir.files[processed++] = vfs_mknode();
        strlcpy(new_node->name, entry.filename, 58);
        if (wfs_file_entry_is_directory(&entry)) {
            new_node->type = VFS_DIR;
            new_node->special.dir.files_in_dir = 0;
            new_node->special.dir.files = NULL;
            new_node->fops = &wfs_directory_fileops;
        } else {
            new_node->type = VFS_DATA;
            new_node->fops = &wfs_file_fileops;
        }
        new_node->size = wfs_file_entry_get_size(&entry);
        new_node->parent = node;

        new_node_info = (wfs_node_info_t *)&new_node->fs_data;
        new_node_info->info = info;
        new_node_info->start_of_file = entry.start_block;
        new_node_info->size = entry.size;
    }

    /* Set out parameters */
    *files_in_dir = node->special.dir.files_in_dir;
    *files = node->special.dir.files;

    return 0;
}

static size_t
wfs_transfer(dev_t *dev, dev_minor_t minor, off_t offset,
             char *buffer, size_t nbytes)
{
  dev_seek(dev, minor, offset, SEEK_SET);
  return dev_read(dev, minor, buffer, nbytes);
}


static uint32_t
wfs_get_block_offset(wfs_info_t *fs, int block)
{
  return WFS_DATA_START + block * WFS_BLOCK_SIZE;
}

static uint16_t
wfs_get_current_block(wfs_node_info_t *ninfo, off_t off,
                      uint16_t *block_position)
{
  off_t o = 0;
  uint16_t block = ninfo->start_of_file;

  while (off >= o + WFS_BLOCK_SIZE)
    {
      block = ninfo->info->block_table[block - 1];
      if (block == WFS_BLOCK_EOF){
				
        return block;
			}
			
      o += WFS_BLOCK_SIZE;
    }

  if (block_position)
    *block_position = off - o;

  return block;
}

static uint16_t
wfs_get_next_block(wfs_info_t *info, uint16_t current_block)
{
  uint16_t block = 0;

  if (current_block == WFS_BLOCK_EOF)
    return current_block;

  if (current_block - 1 >= WFS_N_BLOCKS)
    return WFS_BLOCK_EOF;

  block = info->block_table[current_block - 1];

  return block;
}

static uint16_t
wfs_get_free_block(uint16_t *block_table) {
    uint16_t i;
    for (i = 0; i < WFS_N_BLOCKS; i++)
        if (block_table[i] == WFS_BLOCK_FREE) {
            block_table[i] = WFS_BLOCK_EOF;
            return i;
        }
    printf("WFS: error: could not find a free block in block table: out of space.\n");
    return -1;
}


static size_t
wfs_file_read(vfs_file_t *file, void *buff, size_t len)
{
  int block;
  uint16_t block_position = 0;
  size_t read = 0;
  wfs_node_info_t *ninfo;

  if (file->vnode->type == VFS_DIR)
    return -1;
  
  ninfo = (wfs_node_info_t *)&file->vnode->fs_data;
  block = wfs_get_current_block(ninfo, file->off, &block_position);

  if (block >= WFS_BLOCK_EOF)
    return 0;

  if (file->off >= ninfo->size)
    return 0;

  if (file->off + len > ninfo->size)
    len = ninfo->size - file->off;
  while (len > 0 && block > WFS_BLOCK_FREE && block <= WFS_BLOCK_EOF)
    {
      off_t offset;
      uint16_t transfer;

      offset = wfs_get_block_offset(ninfo->info, block - 1);
 
      if (block_position == 0)
        transfer = WFS_BLOCK_SIZE;
      else
        transfer = WFS_BLOCK_SIZE - block_position;

      if (transfer > len)
        transfer = len;

      transfer = wfs_transfer(ninfo->info->dev, ninfo->info->minor,
                              offset + block_position, buff + read,
                              transfer);
			
      len -= transfer;
      read += transfer;
      file->off += transfer;
      if (len > 0)
        {
          block = wfs_get_next_block(ninfo->info, block);
					
          block_position = 0;

          if (block == WFS_BLOCK_FREE || block >= WFS_BLOCK_EOF)
            {
              printf("WFS: error: corrupted block table.\n");
              break;
            }
        }
    }

  return read;
}

static size_t
wfs_file_write(vfs_file_t *file, const void *buff, size_t len) {
    int block;
    uint16_t block_position = 0;
    size_t write = 0;
    wfs_node_info_t *ninfo;
    off_t offset;
    uint16_t transfer;

    if (file->vnode->type != VFS_DATA) {
        printf("WFS: error: data pointed to is not a file.\n");
        return -1;
    }

    ninfo = (wfs_node_info_t *)&file->vnode->fs_data;

    file->vnode->size = file->off + len;
    ninfo->size = file->off + len;

    block = wfs_get_current_block(ninfo, file->off, &block_position);

    while (len > 0) {
        offset = wfs_get_block_offset(ninfo->info, block - 1);
        transfer = WFS_BLOCK_SIZE - block_position;

        if (transfer > len)
            transfer = len;

        dev_seek(ninfo->info->dev, ninfo->info->minor, offset + block_position, SEEK_SET);
        dev_write(ninfo->info->dev, ninfo->info->minor, buff + write, transfer);

        len -= transfer;
        write += transfer;
        file->off += transfer;

        if (len > 0) {  // Should continue writing in the next block
            if (wfs_get_next_block(ninfo->info, block) == WFS_BLOCK_EOF) {
                int free_block = wfs_get_free_block(ninfo->info->block_table);
                ninfo->info->block_table[block - 1] = free_block;
                block = free_block;
                wfs_write_block_table(ninfo->info);
            } else block = wfs_get_next_block(ninfo->info, block);
            block_position = 0;
        }
        else if (len == 0 && wfs_get_next_block(ninfo->info, block) != WFS_BLOCK_EOF) {
            int help_block;
            block = wfs_get_next_block(ninfo->info, block);
            help_block = ninfo->info->block_table[block - 1];
            ninfo->info->block_table[block] = WFS_BLOCK_EOF;
            while (help_block != WFS_BLOCK_FREE && help_block != WFS_BLOCK_EOF) {
                block = help_block;
                help_block = ninfo->info->block_table[block - 1];
                ninfo->info->block_table[block] = WFS_BLOCK_FREE;
            }
            wfs_write_block_table(ninfo->info);
        }
    }

    return write;
}

static int
wfs_file_open(vfs_vnode_t *vnode)
{
  if (vnode->type == VFS_DIR)
    return -1;

  return 0;
}

static int
wfs_file_close(vfs_file_t *file)
{
  return 0;
}


static void
wfs_read_block_table (wfs_info_t *info)
{
  dev_seek(info->dev, info->minor, WFS_BLOCK_TABLE_START, SEEK_SET);
  dev_read(info->dev, info->minor, info->block_table, WFS_BLOCK_TABLE_SIZE);
}

static void
wfs_write_block_table (wfs_info_t *info)
{
  dev_seek(info->dev, info->minor, WFS_BLOCK_TABLE_START, SEEK_SET);
  dev_write(info->dev, info->minor, info->block_table, WFS_BLOCK_TABLE_SIZE);
}

static int
wfs_mount (vfs_filesys_t *fs,
           vfs_vnode_t *vnode,
           dev_t *dev,
           dev_minor_t minor)
{
  wfs_info_t *info = &wfs_info_array[current_wfs++];

  vnode->special.mount.fs_data = info;
  info->dev = dev;
  info->minor = minor;

  if (dev_open(dev, minor) < 0)
    {
      puts("WFS: error: Cannot open device.");
      return -1;
    }

  info->block_table = vm_map(vm_get_kernel_map(VM_REG_KERNEL_HEAP),
                             VM_SUPER_RW, 0, WFS_BLOCK_TABLE_SIZE);
  wfs_read_block_table(info);

  return 0;
}

static int
wfs_umount(vfs_vnode_t *vnode)
{
  if (vnode->type != VFS_MOUNT)
    return -1;

  /* FIXME: unmap block table */

  dev_close(vnode->special.mount.dev, vnode->special.mount.minor);

  vnode->special.mount.fs = NULL;
  vnode->special.mount.root = NULL;

  return 0;
}

static vfs_vnode_t *
wfs_get_root(vfs_vnode_t *mountpoint)
{
  vfs_vnode_t *root;
  wfs_node_info_t *ninfo;

  root = vfs_mknode();
  root->type = VFS_DIR;
  root->parent = mountpoint;
  root->name[0] = 0;
  root->fs = mountpoint->special.mount.fs;
  root->fops = &wfs_directory_fileops;

  ninfo = (wfs_node_info_t *)&root->fs_data;
  ninfo->info = mountpoint->special.mount.fs_data;

  root->special.dir.files_in_dir = 0;
  root->special.dir.files = NULL;

  return root;
}

static vfs_filesys_t wfs_fs =
{
  "WFS",
  wfs_mount,
  wfs_umount,
  wfs_get_root
};

void
wfs_init(void)
{
  vfs_register(&wfs_fs);
}
